package socket;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**服务端         *///先开服务端Server 后开客户端client
public class Server {
    //java.net.ServerSocket  :运行在服务端 ，主要作用两个
    //1.向系统申请端口，通过该端口与服务器建立连接
    //2.监听服务端口，一旦有客户端连接了，就会立即创建一个Socket对象进行交互
    //如果将Socket比喻为电话，那ServerSocket就相当于客户中心的总机
    private ServerSocket serverSocket;
   // private List<PrintWriter> allOut = new ArrayList<>();
        private   Map<String,PrintWriter> allOut = new HashMap<>();//key是客户端的昵称   value是该客户端的输出流

     public Server(){//类中有成员变量，在构造方法中要赋值
         try {  /**  创建ServerSocket对象时，会申请一个端口
          如果该端口被其它层序占用，会抛出异常
          java.net.BindException:Address already in use
          解决办法：1，更换端口号
          2,杀死占用该端口（通常用于服务器启动两次导致）*/
           System.out.println("正在启动服务端...");
             serverSocket =new ServerSocket(8099); //前面6000个以后的端口都可以用
               System.out.println("服务器启动完毕！");
         } catch (IOException e) {
             e.printStackTrace();//输出堆栈信息
         }


     }
     public void start(){
         try {/** accent():用于 接受客户端连接，并返回一个Socket对象与所连接的客户端进行交互
          该方法是一个阻塞方法，调用后程序会卡住，直接一个客户端连接为止*/
         while(true){
             System.out.println("等待客户端连接...");
             Socket socket = serverSocket.accept();
             System.out.println("一个客户端连接了！");
            //启动单独的线程来处理与客户端的通信
             Clicenthandler handler = new Clicenthandler(socket);      //创建线程
             Thread t = new Thread(handler); //new 一个线程
             t.start();

             //String message = br.readLine();//读取客户端发送过来的字符串
             //System.out.println(message);
         }

         } catch (IOException e) {
             e.printStackTrace();
         }
     }    // 该方法用于启动程序的执行
    public static void main(String[] args) {
        Server server = new Server();
        server.start();
    }   //main其实就是主线程
    /**该线程任务用于负责与指定的客户端通信*/
    private  class Clicenthandler implements Runnable{
        private  Socket socket;

        private String host;//获取客户端的ip地址
        private String nickname;//获取客户端的昵称
        public Clicenthandler(Socket socket){
            this.socket= socket;
            host=socket.getInetAddress().getHostAddress();//通过socket获取该客户端的ip地址
        }
        public  void run(){
            PrintWriter pw = null;
            try {
                // 接收客户端发送过来的信息
                /**  Socket 提供了一个方法
                 * InputStream getInputStream()
                 * 通过socket获取输入流，用于读取客户端发送给过来的数据 */
                InputStream in = socket.getInputStream();
                InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8);
                BufferedReader br = new BufferedReader(isr);

                nickname= br.readLine();////读取客户端发送过来的第一行字符串一定是客户端的昵称
                  /*
                    通过socket获取输出流，用于将消息发送给该客户端
                 */
                OutputStream out = socket.getOutputStream();
                OutputStreamWriter osw = new OutputStreamWriter(out,StandardCharsets.UTF_8);
                BufferedWriter bw = new BufferedWriter(osw);
                pw = new PrintWriter(bw,true);
                //将对应该客户端的输出流存入到共享集合allOut中
                synchronized (allOut) {//同步监视器对象选取临界资源(既然多个线程要操作同一个集合，那就锁该集合)
//                    allOut.add(pw);
                    allOut.put(nickname,pw);
                }
                //广播该客户端上线了
                //范传奇[192.168.1.124]上线了,当前在线人数:2
                broadcast(nickname+"["+host+"]上线了，当前在线人数:"+allOut.size());
                /*
                  当我们通过br.readLine()读取客户端发送过来的数据时，
                  客户端如果调用socket.close()时(进行四次挥手)，正常与我们断开连接
                  那么readLine()会返回null
                  若客户端异常中断(强制杀死进程)，此时readLine()会抛出异常
                  注意:这种情况咱们服务端是没有办法的
                 */


                String message ;  //当我们通过br.readline()方法读取客户端发送过来时的数据时，
                //客户端如果调用socket.close()方法时（进行四次握手）正常与我们断开连接
                //那么readLine()会返回null
                //若客户端异常中断（强制杀死进程），此时readline()会抛出异常
                //注意：这种情况咱们服务端是没有办法的
                while((message=br.readLine())!=null){    //读取客户端发送过来的信息
                    System.out.println("客户端说:"+message);//嵌套循环
                }
            } catch (IOException e) {
             e.printStackTrace();
            } finally {
                //处理该客户端断开链接后的操作
                //将该客户端的输出流从共享集合allOut中删除
                synchronized (allOut) {
//                    allOut.remove(pw);
                    allOut.remove(nickname);
                }
                //广播消息，告知所有客户端该用户下线了
                broadcast(nickname+"["+host+"]下线了，当前在线人数:"+allOut.size());

                try {
                    socket.close(); //进行四次挥手
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        //OutputStream out = scoket.getOutputStream();
        private void broadcast(String message){
            System.out.println(message);
            synchronized (allOut) {
//                for (PrintWriter o : allOut) {
//                    o.println(message);
//                }

                for (PrintWriter o : allOut.values()) {
                    o.println(message);
                }
            }
        }

        /**
         * 私聊，将消息发送给某人
         * @param message
         */
        public void sendMessageToSomeone(String message){
            /*
                过程:
                私聊消息的格式-->@对方昵称:聊天消息
                1:截取出对方的昵称
                  substring()->截"@"到":"之间的内容
                2:从allOut中使用对方的昵称作为key提取到对应的输出流
                  map.get(对方昵称)
                3:将聊天消息通过该输出流发送给对方
                  聊天消息substring()->":"后面的内容
                  然后通过输出流发送给对象
             */
            //首先验证私聊消息是否为正确的格式:@张三:你好
            if(message.matches("@.+:.+")) {
                //1
                String toNickName = message.substring(1, message.indexOf(":"));
                //必要的验证，该用户是否存在
                if(allOut.containsKey(toNickName)) {
                    //2
                    PrintWriter pw = allOut.get(toNickName);
                    //3
                    String toMessage = message.substring(message.indexOf(":") + 1);
                    pw.println(nickname + "悄悄对你说:" + toMessage);
                }else{
                    PrintWriter pw = allOut.get(nickname);
                    pw.println("该用户不存在");
                }
            }else{
                //如果格式不对，给当前用户提示私聊格式不正确
                PrintWriter pw = allOut.get(nickname);
                pw.println("私聊格式不正确，格式应当为 @对方昵称:私聊消息");
            }
        }

    }
}




//线程  城市就比如操作系统  学校的范围就比如是内存   切换班级就比如是线程   切换学校就比如是进程
//在机构里多开班级 就比如是新建Thread0, 新建Thread1, 新建Thread2, 新建Thread3等等 。。。。 也就是多开线程
//run方法运行在哪个线程中，哪个线程调用run方法，run方法就会运行在哪个线程中
//new Thread()的时候还不具备并发的条件、 得调用Start()方法 才会具备并发
//调度器就是分配的
//new 创建了我    调用start()是出生了      早上起床RUNNABLE   晚上回来RUNNING  RUN结束
//线程的优先级    MIN_PRIORITY   MAX_PRIORITY   NORM_PRIORITY
//线程的切换是由线程调度控制的 ，我们无法通过代码来干涉
//谁优先级越高 越光顾谁  时间片越多









